﻿using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ServiceKit.Aop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace ServiceKit.JsonRpc
{
    public class DefaultRpcInvoker : IRpcInvoker
    {
        private IOptions<RpcServerConfiguration> serverConfig { get; }

        private JsonSerializer jsonSerializerCache { get; set; }

        public DefaultRpcInvoker(IOptions<RpcServerConfiguration> serverConfig)
        {
            this.serverConfig = serverConfig;
        }

        public async Task<RpcResponse> InvokeRequestAsync<T>(RpcRequest request, T service, JsonSerializerSettings jsonSerializerSettings = null)
        {
            if (null != jsonSerializerSettings)
            {
                this.jsonSerializerCache = JsonSerializer.Create(jsonSerializerSettings);
            }
            RpcResponse rpcResponse;
            try
            {
                RpcMethodInfo rpcMethod = this.GetMatchingMethod(typeof(T), request);
                if (rpcMethod.IsParametersValid())
                {
                    object result = await this.InvokeAsync(rpcMethod, service);
                    JsonSerializer jsonSerializer = this.GetJsonSerializer();
                    JToken resultJToken = result != null ? JToken.FromObject(result, jsonSerializer) : null;
                    rpcResponse = new RpcResponse(request.Id, resultJToken);
                }
                else
                {
                    RpcError error = new RpcError(new RpcException(500, "参数校验未通过!"), serverConfig.Value.ShowServerException);
                    rpcResponse = new RpcResponse(request.Id, error);
                }
            }
            catch (RpcException ex)
            {
                RpcError error = new RpcError(ex, serverConfig.Value.ShowServerException);
                rpcResponse = new RpcResponse(request.Id, error);
            }
            catch (Exception ex)
            {
                rpcResponse = this.GetUnknownExceptionReponse(request, ex);
            }
            return rpcResponse;
        }

        private async Task<object> InvokeAsync(RpcMethodInfo methodInfo, object service)
        {
            try
            {
                object returnObj = methodInfo.Method.Invoke(service, methodInfo.ConvertedParameters);

                returnObj = await DefaultRpcInvoker.HandleAsyncResponses(returnObj);

                return returnObj;
            }
            catch (Exception ex)
            {
                throw new RpcException(500, ex.Message, null, ex);
            }
        }

        private static async Task<object> HandleAsyncResponses(object returnObj)
        {
            Task task = returnObj as Task;
            if (task == null) //Not async request
            {
                return returnObj;
            }
            try
            {
                await task;
            }
            catch (Exception ex)
            {
                throw new TargetInvocationException(ex);
            }
            PropertyInfo propertyInfo = task.GetType().GetProperty("Result");
            if (propertyInfo != null)
            {
                //Type of Task<T>. Wait for result then return it
                return propertyInfo.GetValue(returnObj);
            }
            //Just of type Task with no return result			
            return null;
        }

        private RpcMethodInfo GetMatchingMethod(Type interfaceType, RpcRequest request)
        {
            List<MethodInfo> methodsWithSameName = this.GetMatchNameMethods(interfaceType, request);

            var potentialMatches = new List<RpcMethodInfo>();
            foreach (MethodInfo method in methodsWithSameName)
            {
                if (this.HasParameterSignature(method, request, out RpcMethodInfo rpcMethodInfo))
                {
                    potentialMatches.Add(rpcMethodInfo);
                }
            }
            RpcMethodInfo rpcMethod = null;

            if (potentialMatches.Count == 1)
            {
                rpcMethod = potentialMatches.First();
            }

            if (rpcMethod == null)
            {
                var methodInfoList = new List<string>();
                foreach (MethodInfo matchedMethod in methodsWithSameName)
                {
                    var parameterTypeList = new List<string>();
                    foreach (ParameterInfo parameterInfo in matchedMethod.GetParameters())
                    {
                        string parameterType = parameterInfo.Name + ": " + parameterInfo.ParameterType.Name;
                        if (parameterInfo.IsOptional)
                        {
                            parameterType += "(Optional)";
                        }
                        parameterTypeList.Add(parameterType);
                    }
                    string parameterString = string.Join(", ", parameterTypeList);
                    methodInfoList.Add($"{{Name: '{matchedMethod.Name}', Parameters: [{parameterString}]}}");
                }
                throw new RpcException(500, string.Join(", ", methodInfoList));
            }

            return rpcMethod;
        }

        private RpcMethodInfo GetMatchingMethod(IEnumerable<object> services, RpcRequest request)
        {
            List<MethodInfo> methodsWithSameName = this.GetMatchNameMethods(services, request);

            var potentialMatches = new List<RpcMethodInfo>();
            foreach (MethodInfo method in methodsWithSameName)
            {
                if (this.HasParameterSignature(method, request, out RpcMethodInfo rpcMethodInfo))
                {
                    potentialMatches.Add(rpcMethodInfo);
                }
            }
            RpcMethodInfo rpcMethod = null;

            if (potentialMatches.Count == 1)
            {
                rpcMethod = potentialMatches.First();
            }

            if (rpcMethod == null)
            {
                var methodInfoList = new List<string>();
                foreach (MethodInfo matchedMethod in methodsWithSameName)
                {
                    var parameterTypeList = new List<string>();
                    foreach (ParameterInfo parameterInfo in matchedMethod.GetParameters())
                    {
                        string parameterType = parameterInfo.Name + ": " + parameterInfo.ParameterType.Name;
                        if (parameterInfo.IsOptional)
                        {
                            parameterType += "(Optional)";
                        }
                        parameterTypeList.Add(parameterType);
                    }
                    string parameterString = string.Join(", ", parameterTypeList);
                    methodInfoList.Add($"{{Name: '{matchedMethod.Name}', Parameters: [{parameterString}]}}");
                }
                throw new RpcException(500, string.Join(", ", methodInfoList));
            }

            return rpcMethod;
        }

        private List<MethodInfo> GetMatchNameMethods(Type interfaceType, RpcRequest request)
        {
            MethodInfo[] allMethods = interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
            List<MethodInfo> methodsWithSameName = new List<MethodInfo>();
            foreach (var method in allMethods)
            {
                if (string.Equals(method.Name, request.Method, StringComparison.OrdinalIgnoreCase))
                {
                    methodsWithSameName.Add(method);
                }
                else
                {
                    RpcMethodAttribute rpcMethodAttr = (RpcMethodAttribute)method.GetCustomAttribute(typeof(RpcMethodAttribute));
                    if (null != rpcMethodAttr && string.Equals(rpcMethodAttr.Name, request.Method, StringComparison.OrdinalIgnoreCase))
                    {
                        methodsWithSameName.Add(method);
                    }
                }
            }
            return methodsWithSameName;
        }

        private List<MethodInfo> GetMatchNameMethods(IEnumerable<object> services, RpcRequest request)
        {
            List<MethodInfo> allMethods = new List<MethodInfo>();
            foreach (var item in services)
            {
                allMethods.AddRange(item.GetType().GetInterfaces()[0].GetMethods(BindingFlags.Public | BindingFlags.Instance));
            }
            List<MethodInfo> methodsWithSameName = new List<MethodInfo>();
            foreach (var method in allMethods)
            {
                RpcMethodAttribute rpcMethodAttr = (RpcMethodAttribute)method.GetCustomAttribute(typeof(RpcMethodAttribute));
                if (null != rpcMethodAttr && string.Equals(rpcMethodAttr.Name, request.Method, StringComparison.OrdinalIgnoreCase))
                {
                    methodsWithSameName.Add(method);
                }
                else
                {
                    throw new Exception(method.DeclaringType.Name+"服务的"+method.Name+ "方法,没有定义RpcMethodAttribute属性");
                }
            }
            return methodsWithSameName;
        }

        private bool HasParameterSignature(MethodInfo method, RpcRequest rpcRequest, out RpcMethodInfo rpcMethodInfo)
        {
            JToken[] orignialParameterList;
            if (rpcRequest.Parameters == null)
            {
                orignialParameterList = new JToken[0];
            }
            else
            {
                switch (rpcRequest.Parameters.Type)
                {
                    case JTokenType.Object:
                        JsonSerializer jsonSerializer = this.GetJsonSerializer();
                        Dictionary<string, JToken> parameterMap = rpcRequest.Parameters.ToObject<Dictionary<string, JToken>>(jsonSerializer);
                        bool canParse = this.TryParseParameterList(method, parameterMap, out orignialParameterList);
                        if (!canParse)
                        {
                            rpcMethodInfo = null;
                            return false;
                        }
                        break;
                    case JTokenType.Array:
                        orignialParameterList = rpcRequest.Parameters.ToArray();
                        break;
                    default:
                        orignialParameterList = new JToken[0];
                        break;
                }
            }
            ParameterInfo[] parameterInfoList = method.GetParameters();
            if (orignialParameterList.Length > parameterInfoList.Length)
            {
                rpcMethodInfo = null;
                throw GetMethodRpcException(method, "传递参数个数大于方法定义的个数");
            }
            object[] correctedParameterList = new object[parameterInfoList.Length];

            for (int i = 0; i < orignialParameterList.Length; i++)
            {
                ParameterInfo parameterInfo = parameterInfoList[i];
                JToken parameter = orignialParameterList[i];
                bool isMatch = this.ParameterMatches(parameterInfo, parameter, out object convertedParameter);
                if (!isMatch)
                {
                    rpcMethodInfo = null;
                    if (rpcRequest.Parameters.Type == JTokenType.Object)
                    {
                        throw GetMethodRpcException(method, "参数类型不匹配");
                    }
                    else
                    {
                        return false;
                    }
                }
                correctedParameterList[i] = convertedParameter;
            }
            if (orignialParameterList.Length < parameterInfoList.Length)
            {
                //make a new array at the same length with padded 'missing' parameters (if optional)
                for (int i = orignialParameterList.Length; i < parameterInfoList.Length; i++)
                {
                    if (!parameterInfoList[i].IsOptional)
                    {
                        rpcMethodInfo = null;
                        throw GetMethodRpcException(method, "缺少参数");
                    }
                    correctedParameterList[i] = Type.Missing;
                }
            }
            rpcMethodInfo = new RpcMethodInfo(method, correctedParameterList, orignialParameterList);
            return true;
        }

        private RpcException GetMethodRpcException(MethodInfo method, string reson)
        {
            var methodInfoList = new List<string>();
            var parameterTypeList = new List<string>();
            foreach (ParameterInfo parameterInfo in method.GetParameters())
            {
                string parameterType = parameterInfo.Name + ": " + parameterInfo.ParameterType.Name;
                if (parameterInfo.IsOptional)
                {
                    parameterType += "(Optional)";
                }
                parameterTypeList.Add(parameterType);
            }
            string parameterString = string.Join(", ", parameterTypeList);
            methodInfoList.Add($"{{Name: '{method.Name}', Parameters: [{parameterString}]}}");
            return new RpcException(500, "找到: " + string.Join(", ", methodInfoList) + reson);
        }

        private bool ParameterMatches(ParameterInfo parameterInfo, JToken value, out object convertedValue)
        {
            Type parameterType = parameterInfo.ParameterType;
            try
            {
                switch (value.Type)
                {
                    case JTokenType.Array:
                        {
                            //TODO should just assume they will work and have the end just fail if cant convert?
                            JsonSerializer serializer = this.GetJsonSerializer();
                            JArray jArray = (JArray)value;
                            convertedValue = jArray.ToObject(parameterType, serializer); //Test conversion
                            return true;
                        }
                    case JTokenType.Object:
                        {
                            //TODO should just assume they will work and have the end just fail if cant convert?
                            JsonSerializer serializer = this.GetJsonSerializer();
                            JObject jObject = (JObject)value;
                            convertedValue = jObject.ToObject(parameterType, serializer); //Test conversion
                            return true;
                        }
                    case JTokenType.String:
                        {
                            if (parameterType == typeof(string))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Integer:
                        {
                            if (parameterType == typeof(int))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Float:
                        {
                            if (parameterType == typeof(float))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Boolean:
                        {
                            if (parameterType == typeof(bool))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Date:
                        {
                            if (parameterType == typeof(DateTime))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Bytes:
                        {
                            if (parameterType == typeof(byte))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.Guid:
                        {
                            if (parameterType == typeof(Guid))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }
                    case JTokenType.TimeSpan:
                        {
                            if (parameterType == typeof(TimeSpan))
                            {
                                convertedValue = value.ToObject(parameterType);
                                return true;
                            }
                            convertedValue = null;
                            return false;
                        }


                    default:
                        convertedValue = value.ToObject(parameterType);
                        return true;
                }
            }
            catch (Exception ex)
            {
                convertedValue = null;
                return false;
            }
        }

        private JsonSerializer GetJsonSerializer()
        {
            if (this.jsonSerializerCache == null)
            {
                return JsonSerializer.CreateDefault();
            }
            return this.jsonSerializerCache;
        }

        private bool TryParseParameterList(MethodInfo method, Dictionary<string, JToken> parametersMap, out JToken[] parameterList)
        {
            ParameterInfo[] parameterInfoList = method.GetParameters();
            parameterList = new JToken[parametersMap.Count()];
            foreach (ParameterInfo parameterInfo in parameterInfoList)
            {
                if (!parametersMap.ContainsKey(parameterInfo.Name) && !parameterInfo.IsOptional)
                {
                    parameterList = null;
                    return false;
                }
                parameterList[parameterInfo.Position] = parametersMap[parameterInfo.Name];
            }
            return true;
        }
        private RpcResponse GetUnknownExceptionReponse(RpcRequest request, Exception ex)
        {
            RpcResponse rpcResponse = new RpcResponse(request.Id, 500, ex.Message);
            return rpcResponse;
        }

        public async Task<RpcResponse> InvokeGateWayRequestAsync(RpcRequest request, IEnumerable<object> services, JsonSerializerSettings jsonSerializerSettings = null)
        {
            if (null != jsonSerializerSettings)
            {
                this.jsonSerializerCache = JsonSerializer.Create(jsonSerializerSettings);
            }
            RpcResponse rpcResponse;
            try
            {
                RpcMethodInfo rpcMethod = this.GetMatchingMethod(services,request);
                if (rpcMethod.IsParametersValid())
                {
                    string className=rpcMethod.Method.DeclaringType.Name;
                    var service = services.SingleOrDefault(x => x.GetType().GetInterfaces()[0].Name == className);
                    object result = await this.InvokeAsync(rpcMethod, service);
                    JsonSerializer jsonSerializer = this.GetJsonSerializer();
                    JToken resultJToken = result != null ? JToken.FromObject(result, jsonSerializer) : null;
                    rpcResponse = new RpcResponse(request.Id, resultJToken);
                }
                else
                {
                    RpcError error = new RpcError(new RpcException(500, "参数校验未通过!"), serverConfig.Value.ShowServerException);
                    rpcResponse = new RpcResponse(request.Id, error);
                }
            }
            catch (RpcException ex)
            {
                RpcError error = new RpcError(ex, serverConfig.Value.ShowServerException);
                rpcResponse = new RpcResponse(request.Id, error);
            }
            catch (Exception ex)
            {
                rpcResponse = this.GetUnknownExceptionReponse(request, ex);
            }
            return rpcResponse;
        }
    }
}
